﻿package cz.dadul.adventure.model
{
	import flash.display.*;
	//import flash.events.*;
	import cz.dadul.adventure.view.*;
	import cz.dadul.adventure.asset.*;
	import cz.dadul.adventure.manager.*;
	import cz.dadul.adventure.model.events.*;
	/**
	 * ...
	 * @author Dadul
	 */
	public class Element extends Sprite
	{		
		// CONSTANTS
		public static const EVENT_TIMELINE:String = "event.timeline";
		public static const FRAME_DELIMITER:String = "_";
		public static const FRAME_PREFIX_STOP:String = "s";
		public static const FRAME_PREFIX_DISPATCH:String = "d";
		public static const FRAME_PREFIX_STOP_AND_DISPATCH:String = "sd";
		
		// VARIABLES		
		// Content movieclip
		protected var _content:MovieClip;
		protected var _labels:Array;
		// State holder (name of a current content frame)
		protected var _state:String;
		// Internal logic behaviour
		protected var _logic:Array;
		// Internal sonic behaviour
		protected var _sonic:Array;
		// Attached eventDispacthers
		protected var _dispatcherNames: Vector.<String>;		
		// Self listener enabled
		protected var _selfListener:Boolean;
		// Enable substates - submovies may change the inner state?
		protected var _substates:Boolean;
		// Specified timeline behaviour
		protected var _prefixBehaviour:Array;
		
		
		public function Element(name:String, mc:MovieClip = null) 
		{
			// Add empy movieclip if not defined
			if (mc == null) mc = new MovieClip();
			
			this._logic = new Array();
			this._labels = new Array();
			
			/**
			 * TODO: Sonic behaviour
			 */
			this._sonic = new Array();			
			// Behaviour 
			this._prefixBehaviour = new Array();			
			this._content = mc;	
				
			// Assign the name
			this.name = name;
			// Self listener
			this._selfListener = false;
			// Substates - if [true], the element state is changed also by all sub-movieclips!
			this._substates = false;
			init();						
		}
		/*
		 * Public functions
		 */
		public function addEventDispatchers(dispatcherNames: Vector.<String>) : void{
			var dispatcher:MovieClip;
			//---------------------------------------
			// Before assign, remove old dispatchers
			if (this._dispatcherNames != null) removeEventDispatchers(dispatcherNames);
			this._dispatcherNames = dispatcherNames;			
			for (var i:String in dispatcherNames) {
				dispatcher = Element.getTimelineComponentByName(dispatcherNames[i]);
				dispatcher.addEventListener(Element.EVENT_TIMELINE, this.onOtherElementEvent);
			}
			
		}
		
		public function addDependencies():void {		
			var dep:Vector.<String> = new Vector.<String>;
			//---------------------------------------------		
			
			// Iterate through all compound component names included in the element's logic
			for (var compCompoundName:String in this._logic) {			
				//  Check self-reference
				if (Element.getComponentTopName(compCompoundName) != this.name){
					// Add component dependency if not listed
					if (dep.lastIndexOf(compCompoundName) == -1) {
						dep.push(compCompoundName);
					}					
				}				
			}
			// If there are some dependencies, add them
			if (dep.length > 0) addEventDispatchers(dep); 
		}
		
		/*
		 * Action public functions
		 */

		/**
		 * Classic gotoAndPlay with automatic frame label handling. 
		 * 
		 * @param	frame			number / string
		 * @param	componentName	name of the target component
		 * @param	component		component itself (for faster processing)
		 */
		public function gotoAndPlay(frame: Object, componentName:String = null, component:DisplayObject = null):void {
			// Determine the component
			if (componentName == null) {
				componentName = this.name;
				component = this._content;
			} else if (componentName == this.name) {
				// Shift the component to the containing movieclip
				component = this._content;
				// Change internal state
				if(frame is String) this._state = frame as String;
			} else if (component == null) {
				// Change internal state (for the substate)
				if((frame is String)&&(this._substates)) this._state = frame as String;				
				component = Element.getTimelineComponentByName(componentName);
			}			
			
			if (frame is String) {			
				// Check frame existence
				if (this._labels[componentName][frame] == null) {
					throw new Error("Component ["+componentName+"] frame ["+frame+"] is not defined");
				}
				// Goto to the correct frame
				MovieClip(component).gotoAndPlay(this._labels[componentName][frame]);
			} else {
				MovieClip(component).gotoAndPlay(frame);
			}
		}
		
		/**
		 * Classic gotoAndStop with automatic frame label handling. 
		 * 
		 * @param	frame			number / string
		 * @param	componentName	name of the target component
		 * @param	component		component itself (for faster processing)
		 */
		public function gotoAndStop(frame: Object, componentName:String = null, component:DisplayObject = null):void {
			// Determine the component
			if (componentName == null) {
				componentName = this.name;
				component = this._content;
			} else if (componentName == this.name) {
				// Shift the component to the containing movieclip
				component = this._content;
				// Change internal state
				if(frame is String) this._state = frame as String;				
			} else if (component == null) {
				// Change internal state (for the substate)
				if((frame is String)&&(this._substates)) this._state = frame as String;
				component = Element.getTimelineComponentByName(componentName);
			}			
			
			if (frame is String) {			
				// Check frame existence
				if (this._labels[componentName][frame] == null) {
					throw new Error("Component ["+componentName+"] frame ["+frame+"] is not defined");
				}				
				// Goto to the correct frame
				MovieClip(component).gotoAndStop(this._labels[componentName][frame]);
			} else {
				MovieClip(component).gotoAndStop(frame);
			}
		}
		
		public function stop():void {
			this._content.stop();
		}
		public function play():void {
			this._content.play();
		}
		
		public function getComponentLabel(comp: String, label: String): String {
			return this._labels[comp][label];
		}
	
		/**
		 * SETTERS / GETTERS
		 */
		public function get content():MovieClip {
			return this._content;
		}
		
		public function set content(mc:MovieClip):void {
			this._content = mc;
		}
		
		public function get state():String {
			return this._state;
		}

		public override function set name(n:String):void {
			super.name = n;
			this._content.name = n;
		}
		public override function get name():String {
			return this._content.name;
		}
		
		public function set logic(l:Array):void {
			this._logic = l;
			// Add dispatcher dependencies
			addDependencies();
		}
		
		public function set substates(b:Boolean):void {
			this._substates = b;
		}
		public function get substates():Boolean{
			return this._substates;
		}
		
		public function get labels():Array {
			return _labels;
		}
		
				
		/**
		 * Basic initialization
		 */
		protected function init():void {
			addChild(this._content);	
			// Initialize timeline of the content movieclip
			Element.initTimeline(this, _content);					
			// Adds content self-listener - it is not dependent on the logic (it changes inner _state)
			this._content.addEventListener(Element.EVENT_TIMELINE, this.onThisElementEvent);
		}
				
		
		protected function removeEventDispatchers(dispatcherNames: Vector.<String>) : void{
			var dispatcher:MovieClip;
			//-------------------------
			for (var i:String in dispatcherNames) {
				dispatcher = Element.getTimelineComponentByName(dispatcherNames[i]);
				dispatcher.removeEventListener(Element.EVENT_TIMELINE, this.onOtherElementEvent);
			}			
		}
				

		/**
		 * Performs set of actions
		 * 
		 * @param	actions		Vector of ElementActionSet or other ation type
		 */
		protected function performAction(action:IActionable):void {
			
			if (!action.perform()) {
				throw(new Error("Element ["+this.name+"] could not perform action: "+action));
			}
		}
		
		
		/**
		 * Event functions
		 */
		protected function onThisElementEvent(e:TimelineEvent):void {
			//trace(this.name + ": "+ e.source);
			
			// Change its own state according to source name
			if (Element.getComponentNumber(e.component) == 1) {
				// Top-most component call
				this._state = e.source;
			} else if (this._substates) {
				// Sub-component call
				this._state = e.source;
			}
			
			// The same behaviour as otherLementEvent
			onOtherElementEvent(e);			 
		}
		
		protected function onOtherElementEvent(e:TimelineEvent):void {			
			if ((this._logic[e.component] != null) && (this._logic[e.component][e.source]) != null) {
				// Check state-dependent action or default action
				if (this._logic[e.component][e.source][_state] != null) {
					performAction(this._logic[e.component][e.source]);		
				} else {
					// *default* action
					if (this._logic[e.component][e.source][ElementActionSet.DEFAULT] != null) {
						performAction(this._logic[e.component][e.source][ElementActionSet.DEFAULT]);
					} else {
						throw new Error("["+this.name+"] Missing default action for the component ["+e.component+"] and source ["+e.source+"]!");
					}
				}
			}
		}
		
		protected function addPrefixBehaviour(prefix: String, fn: Function):void {
			if (this._prefixBehaviour[prefix] == null) {
				this._prefixBehaviour[prefix] = fn;	
			} else {
				throw new Error("Item ["+this.name+"] behaviour for the prefix ["+prefix+"] has been already defined!");
			}
			
		}
		
		/**
		 * STATIC 
		 */
		protected static function initTimeline(element: Element, component:MovieClip, componentName:String = null):void {
			var num:uint = 	component.numChildren;
			// Concatenation of the component name
			var componentName:String = (componentName == null) ? component.name : componentName + "." + component.name;
			//------------------------------------
			
			for (var i:uint = 0; i < num; i++) {
				// Iterate through *ONLY* movieclips				
				if(component.getChildAt(i) is MovieClip) Element.initTimeline(element, component.getChildAt(i) as MovieClip, componentName);
			}
			// Initialize timeline and receive labels object
			element._labels[componentName] = Element.initTimelineFrames(element, component, componentName);			 
		}		
		
		protected static function  initTimelineFrames(element:Element, component:MovieClip, componentName: String):Object {
			// Initialize and receive labels
			var labels:Object = FrameScriptManager.getLabels(component);	
			var labelSplit:Array;
			var prefix:String;
			var frameName:String;
			var action:Function;
			//-------------------------------		
			for (var label:* in labels) {				
				labelSplit = label.split(Element.FRAME_DELIMITER);
				// Pass new cycle if the prefix is not defined
				if (labelSplit.length == 1) continue;
				prefix = labelSplit[0];
				frameName = labelSplit[1];
				switch(prefix) {
					case(Element.FRAME_PREFIX_STOP):
						action = function():void {
							component.stop();
						}
						FrameScriptManager.setFrameScript(component, new FrameScript(label, action));
					break;
					case(Element.FRAME_PREFIX_DISPATCH):
						action = function(frameName:String):Function {
							return function():void {
								component.dispatchEvent(new TimelineEvent(Element.EVENT_TIMELINE, componentName, frameName));
							}
						}						
						FrameScriptManager.setFrameScript(component, new FrameScript(label, action(frameName)));
					break;						
					case(Element.FRAME_PREFIX_STOP_AND_DISPATCH):
						action = function(frameName:String):Function {
							return function():void {
								component.stop();
								component.dispatchEvent(new TimelineEvent(Element.EVENT_TIMELINE, componentName, frameName));
							}
						}						
						FrameScriptManager.setFrameScript(component, new FrameScript(label, action(frameName)));
					break;	
					default:
						/* Default NOT-ELEMENT-DEFINED behaviour -> the element will
						 * try to load it from externally defined behaviour if
						 * the component is the root.
						 */ 
						if (Element.getComponentNumber(componentName) == 1) {								
							if (element._prefixBehaviour[prefix] is Function) {
								// The behaviour is defined -> call it								
								// The call should return the function
								action = element._prefixBehaviour[prefix](element,frameName);
								FrameScriptManager.setFrameScript(component, new FrameScript(label, action));
							}
						}											
					break;
				}
				
			} // for-in
						
			
			// Return component parsed labels
			return FrameScriptManager.getParsedLabels();
		}
		
		/**
		 * Finds a returns movieClip by the givend compoundName path (i.e.: my_clip1.my_clip2.my_clip3)
		 * It always returns timeline component, thus (sub)content of the Item.
		 * 
		 * Its usage is dedicated to timeline listening as for the EventDispatchers
		 * 
		 * @param	compoundName
		 * @return	movieClip		content of the component
		 */
		public static function getTimelineComponentByName(compoundName:String):MovieClip {
			var layerController:LayerController = LayerController.getInstance();
			var component:MovieClip = null;
			var compNames:Array = compoundName.split(".");					
			//----------------------------------
			// Get the component from the layer (its content)
			/*
			 * Warning: It takes ALWAYS content of the component (item)
			 */ 
			component = layerController.getElement(compNames[0]).content;
			// Traverse for the children
			for (var i:uint = 1; i < compNames.length; i++) {
				component = component.getChildByName(compNames[i]) as MovieClip;
			}						
			return component;
		}
		
		/**
		 * Finds a returns displayObject by the givend compoundName path (i.e.: my_clip1.my_clip2.my_clip3)
		 * It returns component itself (NOT the content) or its subcontent
		 * 
		 * Its usage is dedicated to action listening as for the ElementActionSet
		 * 
		 * @param	compoundName
		 * @return	displayObject	component itself (or subcomponents)
		 */
		public static function getActionComponentByName(compoundName:String):DisplayObject {
			var layerController:LayerController = LayerController.getInstance();
			var component:DisplayObject = null;
			var subcomponent:MovieClip = null;
			var compNames:Array = compoundName.split(".");					
			//----------------------------------
			// Get the component from the layer (WITHOUT content)
			component = layerController.getElement(compNames[0]);
			
			// Is it the reference to subcomponents?
			if (compNames.length > 1) {
				subcomponent = layerController.getElement(compNames[0]).content;
				// Traverse for the children
				for (var i:uint = 1; i < compNames.length; i++) {
					subcomponent = subcomponent.getChildByName(compNames[i]) as MovieClip;
				}										
				component = subcomponent;
			}
			return component;
		}
		
		
		public static function getTopComponent(compoundName:String):DisplayObject {
			var layerController:LayerController = LayerController.getInstance();
			//------------------------------------------------------------------
			var topName:String = Element.getComponentTopName(compoundName);
			return layerController.getElement(topName);
		}
		
		/**
		 * Returnds top-most name of the component
		 * 
		 * @param	compoundName
		 * @return  left-most (top-most) name of the component
		 */
		public static function getComponentTopName(compoundName:String):String {
			var compNames:Array = compoundName.split(".");
			return compNames[0];
		}
		/**
		 * Returns number of the sumbcomponentns (including the top-most component)
		 * 
		 * @param	compoundName
		 * @return	number	of subcompnents (including the top-most component)
		 */
		public static function getComponentNumber(compoundName:String):uint {
			var compNames:Array = compoundName.split(".");
			return compNames.length;			
		}
		

						
	}

}